#!/usr/bin/env bash
#
# This script can be used to interact with kolla via ansible.

set -o errexit

# do not use _PYTHON_BIN directly, use $(get_python_bin) instead
_PYTHON_BIN=""

ANSIBLE_VERSION_MIN=2.14
ANSIBLE_VERSION_MAX=2.15

function get_python_bin {
    if [ -n "$_PYTHON_BIN" ]; then
      echo -n "$_PYTHON_BIN"
      return
    fi

    local ansible_path
    ansible_path=$(which ansible)

    if [[ $? -ne 0 ]]; then
        echo "ERROR: Ansible is not installed in the current (virtual) environment." >&2
        echo "Ansible version should be between $ANSIBLE_VERSION_MIN and $ANSIBLE_VERSION_MAX." >&2
        exit 1
    fi

    local ansible_shebang_line
    ansible_shebang_line=$(head -n1 "$ansible_path")

    if ! echo "$ansible_shebang_line" | egrep "^#!" &>/dev/null; then
        echo "ERROR: Ansible script is malformed (missing shebang line)." >&2
        exit 1
    fi

    # NOTE(yoctozepto): may have multiple parts
    _PYTHON_BIN=${ansible_shebang_line#\#\!}
    echo -n "$_PYTHON_BIN"
}

function check_environment_coherence {
    local ansible_python_cmdline
    ansible_python_cmdline=$(get_python_bin)
    ansible_python_version=$($ansible_python_cmdline -c 'import sys; print(str(sys.version_info[0])+"."+str(sys.version_info[1]))')

    if ! $ansible_python_cmdline --version &>/dev/null; then
        echo "ERROR: Ansible Python is not functional." >&2
        echo "Tried '$ansible_python_cmdline'" >&2
        exit 1
    fi

    # Check for existence of kolla_ansible module using Ansible's Python.
    if ! $ansible_python_cmdline -c 'import kolla_ansible' &>/dev/null; then
        echo "ERROR: kolla_ansible has to be available in the Ansible PYTHONPATH." >&2
        echo "Please install both in the same (virtual) environment." >&2
        exit 1
    fi

    local ansible_full_version
    ansible_full_version=$($ansible_python_cmdline -c 'import ansible; print(ansible.__version__)')

    if [[ $? -ne 0 ]]; then
        echo "ERROR: Failed to obtain Ansible version:" >&2
        echo "$ansible_full_version" >&2
        exit 1
    fi

    local ansible_version
    ansible_version=$(echo "$ansible_full_version" | egrep -o '^[0-9]+\.[0-9]+')

    if [[ $? -ne 0 ]]; then
        echo "ERROR: Failed to parse Ansible version:" >&2
        echo "$ansible_full_version" >&2
        exit 1
    fi


    if [[ $(printf "%s\n" "$ANSIBLE_VERSION_MIN" "$ANSIBLE_VERSION_MAX" "$ansible_version" | sort -V | head -n1) != "$ANSIBLE_VERSION_MIN" ]] ||
       [[ $(printf "%s\n" "$ANSIBLE_VERSION_MIN" "$ANSIBLE_VERSION_MAX" "$ansible_version" | sort -V | tail -n1) != "$ANSIBLE_VERSION_MAX" ]]; then
        echo "ERROR: Ansible version should be between $ANSIBLE_VERSION_MIN and $ANSIBLE_VERSION_MAX. Current version is $ansible_full_version which is not supported."
        exit 1
    fi
}

function find_base_dir {
    local dir_name
    local python_dir
    dir_name=$(dirname "$0")
    # NOTE(yoctozepto): Fix the case where dir_name is a symlink and VIRTUAL_ENV might not be. This
    # happens with pyenv-virtualenv, see https://bugs.launchpad.net/kolla-ansible/+bug/1903887
    dir_name=$(readlink -e "$dir_name")
    python_dir="python${ansible_python_version}"
    if [ -z "$SNAP" ]; then
        if [[ ${dir_name} == "/usr/bin" ]]; then
            if test -f /usr/lib/${python_dir}/*-packages/kolla-ansible.egg-link; then
                # Editable install.
                BASEDIR="$(head -n1 /usr/lib/${python_dir}/*-packages/kolla-ansible.egg-link)"
            else
                BASEDIR=/usr/share/kolla-ansible
            fi
        elif [[ ${dir_name} == "/usr/local/bin" ]]; then
            if test -f /usr/local/lib/${python_dir}/*-packages/kolla-ansible.egg-link; then
                # Editable install.
                BASEDIR="$(head -n1 /usr/local/lib/${python_dir}/*-packages/kolla-ansible.egg-link)"
            else
                BASEDIR=/usr/local/share/kolla-ansible
            fi
        elif [[ ${dir_name} == ~/.local/bin ]]; then
            if test -f ~/.local/lib/${python_dir}/*-packages/kolla-ansible.egg-link; then
                # Editable install.
                BASEDIR="$(head -n1 ~/.local/lib/${python_dir}/*-packages/kolla-ansible.egg-link)"
            else
                BASEDIR=~/.local/share/kolla-ansible
            fi
        elif [[ -n ${VIRTUAL_ENV} ]] && [[ ${dir_name} == "$(readlink -e "${VIRTUAL_ENV}/bin")" ]]; then
            if test -f ${VIRTUAL_ENV}/lib/${python_dir}/site-packages/kolla-ansible.egg-link; then
                # Editable install.
                BASEDIR="$(head -n1 ${VIRTUAL_ENV}/lib/${python_dir}/*-packages/kolla-ansible.egg-link)"
            else
                BASEDIR="${VIRTUAL_ENV}/share/kolla-ansible"
            fi
        else
            # Running from sources (repo).
            BASEDIR="$(dirname ${dir_name})"
        fi
    else
        BASEDIR="$SNAP/share/kolla-ansible"
    fi
}

function install_deps {
    echo "Installing Ansible Galaxy dependencies"
    if pip show ansible 2>/dev/null; then
        ansible-galaxy collection install -r ${BASEDIR}/requirements.yml --force
    else
        ansible-galaxy collection install -r ${BASEDIR}/requirements.yml --force
        ansible-galaxy collection install -r ${BASEDIR}/requirements-core.yml --force
    fi

    if [[ $? -ne 0 ]]; then
        echo "ERROR: Failed to install Ansible Galaxy dependencies" >&2
        exit 1
    fi
}

function process_cmd {
    echo "$ACTION : $CMD"
    $CMD
    if [[ $? -ne 0 ]]; then
        echo "Command failed $CMD"
        exit 1
    fi
}

function usage {
    cat <<EOF
Usage: $0 COMMAND [options]

Options:
    --inventory, -i <inventory_path>   Specify path to ansible inventory file. \
Can be specified multiple times to pass multiple inventories.
    --playbook, -p <playbook_path>     Specify path to ansible playbook file
    --configdir <config_path>          Specify path to directory with globals.yml
    --key -k <key_path>                Specify path to ansible vault keyfile
    --help, -h                         Show this usage information
    --tags, -t <tags>                  Only run plays and tasks tagged with these values
    --skip-tags <tags>                 Only run plays and tasks whose tags do not match these values
    --extra, -e <ansible variables>    Set additional variables as key=value or YAML/JSON passed to ansible-playbook
    --passwords <passwords_path>       Specify path to the passwords file
    --limit <host>                     Specify host to run plays
    --forks <forks>                    Number of forks to run Ansible with
    --vault-id <@prompt or path>       Specify @prompt or password file (Ansible >=  2.4)
    --ask-vault-pass                   Ask for vault password
    --vault-password-file <path>       Specify password file for vault decrypt
    --check, -C                        Don't make any changes and try to predict some of the changes that may occur instead
    --diff, -D                         Show differences in ansible-playbook changed tasks
    --verbose, -v                      Increase verbosity of ansible-playbook
    --version                          Show version

Environment variables:
    EXTRA_OPTS                         Additional arguments to pass to ansible-playbook

Commands:
    install-deps         Install Ansible Galaxy dependencies
    prechecks            Do pre-deployment checks for hosts
    mariadb_recovery     Recover a completely stopped mariadb cluster
    mariadb_backup       Take a backup of MariaDB databases
                             --full (default)
                             --incremental
    bootstrap-servers    Bootstrap servers with kolla deploy dependencies
    destroy              Destroy Kolla containers, volumes and host configuration
                             --include-images to also destroy Kolla images
                             --include-dev to also destroy dev mode repos
    deploy               Deploy and start all kolla containers
    deploy-bifrost       Deploy and start bifrost container
    deploy-servers       Enroll and deploy servers with bifrost
    deploy-containers    Only deploy and start containers (no config updates or bootstrapping)
    gather-facts         Gather Ansible facts
    post-deploy          Do post deploy on deploy node
    pull                 Pull all images for containers (only pulls, no running container changes)
    rabbitmq-reset-state Force reset the state of RabbitMQ
    reconfigure          Reconfigure OpenStack service
    stop                 Stop Kolla containers
    certificates         Generate self-signed certificate for TLS *For Development Only*
    octavia-certificates Generate certificates for octavia deployment
                             --check-expiry <days> to check if certificates expire within that many days
    upgrade              Upgrades existing OpenStack Environment
    upgrade-bifrost      Upgrades an existing bifrost container
    genconfig            Generate configuration files for enabled OpenStack services
    validate-config      Validate configuration files for enabled OpenStack services
    prune-images         Prune orphaned Kolla images
    nova-libvirt-cleanup Clean up disabled nova_libvirt containers
EOF
}

function bash_completion {
cat <<EOF
--inventory -i
--playbook -p
--configdir
--key -k
--help -h
--skip-tags
--tags -t
--extra -e
--passwords
--limit
--forks
--vault-id
--ask-vault-pass
--vault-password-file
--check -C
--diff -D
--verbose -v
--version
install-deps
prechecks
mariadb_recovery
mariadb_backup
bootstrap-servers
destroy
deploy
deploy-bifrost
deploy-containers
deploy-servers
gather-facts
post-deploy
pull
rabbitmq-reset-state
reconfigure
stop
certificates
octavia-certificates
upgrade
upgrade-bifrost
genconfig
validate-config
prune-images
nova-libvirt-cleanup
EOF
}

function version {
    local python_bin
    python_bin=$(get_python_bin)

    $python_bin -c 'from kolla_ansible.version import version_info; print(version_info)'
}

check_environment_coherence

SHORT_OPTS="hi:p:t:k:e:CD:v"
LONG_OPTS="help,version,inventory:,playbook:,skip-tags:,tags:,key:,extra:,check,diff,verbose,configdir:,passwords:,limit:,forks:,vault-id:,ask-vault-pass,vault-password-file:,yes-i-really-really-mean-it,include-images,include-dev:,full,incremental,check-expiry:"

RAW_ARGS="$*"
ARGS=$(getopt -o "${SHORT_OPTS}" -l "${LONG_OPTS}" --name "$0" -- "$@") || { usage >&2; exit 2; }

eval set -- "$ARGS"

find_base_dir

INVENTORY="${BASEDIR}/ansible/inventory/all-in-one"
PLAYBOOK="${BASEDIR}/ansible/site.yml"
VERBOSITY=
EXTRA_OPTS=${EXTRA_OPTS}
CONFIG_DIR="/etc/kolla"
DANGER_CONFIRM=
INCLUDE_IMAGES=
INCLUDE_DEV=
BACKUP_TYPE="full"
OCTAVIA_CERTS_EXPIRY=
# Serial is not recommended and disabled by default. Users can enable it by
# configuring ANSIBLE_SERIAL variable.
ANSIBLE_SERIAL=${ANSIBLE_SERIAL:-0}
INVENTORIES=()

while [ "$#" -gt 0 ]; do
    case "$1" in

    (--inventory|-i)
            INVENTORIES+=("$2")
            shift 2
            ;;

    (--playbook|-p)
            PLAYBOOK="$2"
            shift 2
            ;;

    (--skip-tags)
            EXTRA_OPTS="$EXTRA_OPTS --skip-tags $2"
            shift 2
            ;;

    (--tags|-t)
            EXTRA_OPTS="$EXTRA_OPTS --tags $2"
            shift 2
            ;;

    (--check|-C)
            EXTRA_OPTS="$EXTRA_OPTS --check"
            shift 1
            ;;

    (--diff|-D)
            EXTRA_OPTS="$EXTRA_OPTS --diff"
            shift 1
            ;;

    (--verbose|-v)
            VERBOSITY="$VERBOSITY --verbose"
            shift 1
            ;;

    (--configdir)
            CONFIG_DIR="$2"
            shift 2
            ;;

    (--yes-i-really-really-mean-it)
            if [[ ${RAW_ARGS} =~ "$1" ]]
            then
                DANGER_CONFIRM="$1"
            fi
            shift 1
            ;;

    (--include-images)
            INCLUDE_IMAGES="$1"
            shift 1
            ;;

    (--include-dev)
            INCLUDE_DEV="$1"
            shift 1
            ;;

    (--key|-k)
            VAULT_PASS_FILE="$2"
            EXTRA_OPTS="$EXTRA_OPTS --vault-password-file=$VAULT_PASS_FILE"
            shift 2
            ;;

    (--extra|-e)
            EXTRA_OPTS="$EXTRA_OPTS -e $2"
            shift 2
            ;;

    (--passwords)
            PASSWORDS_FILE="$2"
            shift 2
            ;;

    (--limit)
            EXTRA_OPTS="$EXTRA_OPTS --limit $2"
            shift 2
            ;;

    (--forks)
            EXTRA_OPTS="$EXTRA_OPTS --forks $2"
            shift 2
            ;;

    (--vault-id)
            EXTRA_OPTS="$EXTRA_OPTS --vault-id $2"
            shift 2
            ;;

    (--ask-vault-pass)
            VERBOSITY="$EXTRA_OPTS --ask-vault-pass"
            shift 1
            ;;

    (--vault-password-file)
            EXTRA_OPTS="$EXTRA_OPTS --vault-password-file $2"
            shift 2
            ;;

    (--full)
            BACKUP_TYPE="full"
            shift 1
            ;;

    (--incremental)
            BACKUP_TYPE="incremental"
            shift 1
            ;;

    (--check-expiry)
            OCTAVIA_CERTS_EXPIRY="$2"
            shift 2
            ;;

    (--version)
            version
            exit 0
            ;;

    (--help|-h)
            usage
            exit 0
            ;;

    (--)
            shift
            break
            ;;

    (*)
            echo "error"
            exit 3
            ;;
esac
done

case "$1" in

(install-deps)
        install_deps
        exit 0
        ;;
(prechecks)
        ACTION="Pre-deployment checking"
        EXTRA_OPTS="$EXTRA_OPTS -e kolla_action=precheck"
        ;;
(mariadb_recovery)
        ACTION="Attempting to restart mariadb cluster"
        EXTRA_OPTS="$EXTRA_OPTS -e kolla_action=deploy"
        PLAYBOOK="${BASEDIR}/ansible/mariadb_recovery.yml"
        ;;
(mariadb_backup)
        ACTION="Backup MariaDB databases"
        EXTRA_OPTS="$EXTRA_OPTS -e kolla_action=backup -e mariadb_backup_type=${BACKUP_TYPE}"
        PLAYBOOK="${BASEDIR}/ansible/mariadb_backup.yml"
        ;;
(destroy)
        ACTION="Destroy Kolla containers, volumes and host configuration"
        PLAYBOOK="${BASEDIR}/ansible/destroy.yml"
        if [[ "${INCLUDE_IMAGES}" == "--include-images" ]]; then
            EXTRA_OPTS="$EXTRA_OPTS -e destroy_include_images=yes"
        fi
        if [[ "${INCLUDE_DEV}" == "--include-dev" ]]; then
            EXTRA_OPTS="$EXTRA_OPTS -e destroy_include_dev=yes"
        fi
        if [[ "${DANGER_CONFIRM}" != "--yes-i-really-really-mean-it" ]]; then
            cat << EOF
WARNING:
    This will PERMANENTLY DESTROY all deployed kolla containers, volumes and host configuration.
    There is no way to recover from this action. To confirm, please add the following option:
    --yes-i-really-really-mean-it
EOF
            exit 1
        fi
        ;;
(bootstrap-servers)
        ACTION="Bootstrapping servers"
        PLAYBOOK="${BASEDIR}/ansible/kolla-host.yml"
        EXTRA_OPTS="$EXTRA_OPTS -e kolla_action=bootstrap-servers"
        ;;
(deploy)
        ACTION="Deploying Playbooks"
        EXTRA_OPTS="$EXTRA_OPTS -e kolla_action=deploy"
        ;;
(deploy-bifrost)
        ACTION="Deploying Bifrost"
        PLAYBOOK="${BASEDIR}/ansible/bifrost.yml"
        EXTRA_OPTS="$EXTRA_OPTS -e kolla_action=deploy"
        ;;
(deploy-containers)
        ACTION="Deploying Containers"
        EXTRA_OPTS="$EXTRA_OPTS -e kolla_action=deploy-containers"
        ;;
(deploy-servers)
        ACTION="Deploying servers with bifrost"
        PLAYBOOK="${BASEDIR}/ansible/bifrost.yml"
        EXTRA_OPTS="$EXTRA_OPTS -e kolla_action=deploy-servers"
        ;;
(gather-facts)
        ACTION="Gathering Ansible facts"
        PLAYBOOK="${BASEDIR}/ansible/gather-facts.yml"
        ;;
(post-deploy)
        ACTION="Post-Deploying Playbooks"
        PLAYBOOK="${BASEDIR}/ansible/post-deploy.yml"
        ;;
(pull)
        ACTION="Pulling Docker images"
        EXTRA_OPTS="$EXTRA_OPTS -e kolla_action=pull"
        ;;
(upgrade)
        ACTION="Upgrading OpenStack Environment"
        EXTRA_OPTS="$EXTRA_OPTS -e kolla_action=upgrade -e kolla_serial=${ANSIBLE_SERIAL}"
        ;;
(upgrade-bifrost)
        ACTION="Upgrading Bifrost"
        PLAYBOOK="${BASEDIR}/ansible/bifrost.yml"
        EXTRA_OPTS="$EXTRA_OPTS -e kolla_action=upgrade"
        ;;
(reconfigure)
        ACTION="Reconfigure OpenStack service"
        EXTRA_OPTS="$EXTRA_OPTS -e kolla_action=reconfigure -e kolla_serial=${ANSIBLE_SERIAL}"
        ;;
(stop)
        ACTION="Stop Kolla containers"
        EXTRA_OPTS="$EXTRA_OPTS -e kolla_action=stop"
        if [[ "${DANGER_CONFIRM}" != "--yes-i-really-really-mean-it" ]]; then
            cat << EOF
WARNING:
    This will stop all deployed kolla containers, limit with tags is possible and also with
    skip_stop_containers variable. To confirm, please add the following option:
    --yes-i-really-really-mean-it
EOF
            exit 1
        fi
        ;;
(certificates)
        ACTION="Generate TLS Certificates"
        PLAYBOOK="${BASEDIR}/ansible/certificates.yml"
        ;;
(octavia-certificates)
        ACTION="Generate octavia Certificates"
        PLAYBOOK="${BASEDIR}/ansible/octavia-certificates.yml"
        if [[ ! -z "${OCTAVIA_CERTS_EXPIRY}" ]]; then
            EXTRA_OPTS="$EXTRA_OPTS -e octavia_certs_check_expiry=yes -e octavia_certs_expiry_limit=${OCTAVIA_CERTS_EXPIRY}"
        fi
        ;;
(genconfig)
        ACTION="Generate configuration files for enabled OpenStack services"
        EXTRA_OPTS="$EXTRA_OPTS -e kolla_action=config"
        ;;
(validate-config)
        ACTION="Validate configuration files for enabled OpenStack services"
        EXTRA_OPTS="$EXTRA_OPTS -e kolla_action=config_validate"
        ;;
(prune-images)
        ACTION="Prune orphaned Kolla images"
        PLAYBOOK="${BASEDIR}/ansible/prune-images.yml"
        if [[ "${DANGER_CONFIRM}" != "--yes-i-really-really-mean-it" ]]; then
            cat << EOF
WARNING:
    This will PERMANENTLY DELETE all orphaned kolla images. To confirm, please add the following option:
    --yes-i-really-really-mean-it
EOF
            exit 1
        fi
        ;;
(nova-libvirt-cleanup)
        ACTION="Cleanup disabled nova_libvirt containers"
        PLAYBOOK="${BASEDIR}/ansible/nova-libvirt-cleanup.yml"
        ;;
(rabbitmq-reset-state)
        ACTION="Force reset the state of RabbitMQ"
        PLAYBOOK="${BASEDIR}/ansible/rabbitmq-reset-state.yml"
        ;;
(bash-completion)
        bash_completion
        exit 0
        ;;
(*)     usage
        exit 3
        ;;
esac

GLOBALS_DIR="${CONFIG_DIR}/globals.d"
EXTRA_GLOBALS=$([ -d "${GLOBALS_DIR}" ] && find ${GLOBALS_DIR} -maxdepth 1 -type f -name '*.yml' -printf ' -e @%p' || true 2>/dev/null)
PASSWORDS_FILE="${PASSWORDS_FILE:-${CONFIG_DIR}/passwords.yml}"
CONFIG_OPTS="-e @${CONFIG_DIR}/globals.yml ${EXTRA_GLOBALS} -e @${PASSWORDS_FILE} -e CONFIG_DIR=${CONFIG_DIR}"
CMD="ansible-playbook $CONFIG_OPTS $EXTRA_OPTS $PLAYBOOK $VERBOSITY"
for INVENTORY in ${INVENTORIES[@]}; do
    CMD="${CMD} --inventory $INVENTORY"
done
process_cmd
